package ace.cmp.data.methodname.method.name.sql.core.resolver.impl;

import ace.cmp.data.methodname.method.name.sql.core.enums.LogicalOpEnum;
import ace.cmp.data.methodname.method.name.sql.core.enums.RelationalOpEnum;
import ace.cmp.data.methodname.method.name.sql.core.model.MethodNameToQueryFilter;
import ace.cmp.data.methodname.method.name.sql.core.model.MethodNameToQuerySort;
import ace.cmp.data.methodname.method.name.sql.core.resolver.MethodNameToPredicateResolver;
import ace.cmp.data.methodname.method.name.sql.core.resolver.MethodNameToSubjectResolver;
import ace.cmp.data.methodname.method.name.sql.core.util.AssertUtils;
import ace.cmp.data.methodname.method.name.sql.core.util.StringUtils;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
 * @author caspar
 * @date 2023/2/18 16:27
 */
public class MethodNameToPredicateResolverImpl implements MethodNameToPredicateResolver {

  private static final String KEYWORD_ORDER_BY_PATTERN_VALUE = "OrderBy";
  private static final Pattern KEYWORD_ORDER_BY_PATTERN =
      Pattern.compile(KEYWORD_ORDER_BY_PATTERN_VALUE);
  private static final String KEYWORD_AND_PATTERN_VALUE = String.format("And(?=[A-Z])");
  private static final Pattern KEYWORD_AND_PATTERN = Pattern.compile(KEYWORD_AND_PATTERN_VALUE);
  private static final String KEYWORD_OR_PATTERN_VALUE = String.format("Or(?=[A-Z])");
  private static final Pattern KEYWORD_OR_PATTERN = Pattern.compile(KEYWORD_OR_PATTERN_VALUE);
  private static final Pattern KEYWORD_ASC_DESC_SPLIT_PATTERN =
      Pattern.compile("(?<=Asc|Desc)(?=.*)");
  private static final Pattern KEYWORD_DIRECTION_SPLIT_PATTERN =
      Pattern.compile("(.+?)(Asc|Desc)?$");
  private static final String INVALID_ORDER_SYNTAX = "Invalid order syntax for part %s";
  private static final Set<String> DIRECTION_KEYWORDS = new HashSet<>(Arrays.asList("Asc", "Desc"));

  private final MethodNameToSubjectResolver subjectResolver;
  private final String predicateSourceString;
  private final String filterString;
  private final String orderByString;
  private final String methodName;

  public MethodNameToPredicateResolverImpl(String methodName) {
    AssertUtils.notNull(methodName, "methodName must not be null");
    this.subjectResolver = new MethodNameToSubjectResolverImpl(methodName);
    this.methodName = subjectResolver.getMethodName();
    this.predicateSourceString =
        this.methodName.substring(subjectResolver.getSubjectSourceString().length());
    List<String> filterAndOrderBySourceStringList =
        Arrays.stream(KEYWORD_ORDER_BY_PATTERN.split(this.predicateSourceString))
            .collect(Collectors.toList());
    AssertUtils.isTrue(
        filterAndOrderBySourceStringList.size() < 3,
        "OrderBy key word is can only one. " + this.methodName);
    this.filterString =
        filterAndOrderBySourceStringList.size() > 0
            ? filterAndOrderBySourceStringList.get(0).replaceFirst("By", StringUtils.EMPTY)
            : StringUtils.EMPTY;
    this.orderByString =
        filterAndOrderBySourceStringList.size() > 1
            ? filterAndOrderBySourceStringList.get(1)
            : StringUtils.EMPTY;
  }

  @Override
  public String getMethodName() {
    return this.methodName;
  }

  @Override
  public String getPredicateSourceString() {
    return predicateSourceString;
  }

  @Override
  public List<MethodNameToQueryFilter> getFilters() {
    if (StringUtils.isEmpty(this.filterString)) {
      return new ArrayList<>();
    }
    return Arrays.stream(KEYWORD_AND_PATTERN.split(this.filterString))
        .map(
            andPart -> {
              List<String> filterWithKeywordList =
                  Arrays.stream(KEYWORD_OR_PATTERN.split(andPart)).collect(Collectors.toList());
              String column;
              LogicalOpEnum logicalOp;
              RelationalOpEnum relationalOp;
              List<MethodNameToQueryFilter> filters = new ArrayList<>(filterWithKeywordList.size());
              for (int i = 0; i < filterWithKeywordList.size(); i++) {
                String filterWithKeyword = filterWithKeywordList.get(i);
                logicalOp = i == 0 ? LogicalOpEnum.AND : LogicalOpEnum.OR;
                column = this.resolveColumn(filterWithKeyword);
                relationalOp = this.resolveRelationalOp(filterWithKeyword);
                MethodNameToQueryFilter filter =
                    MethodNameToQueryFilter.builder()
                        .propertyPathMeta(column)
                        .logicalOp(logicalOp)
                        .relationalOp(relationalOp)
                        .build();
                filters.add(filter);
              }
              return filters;
            })
        .flatMap(List::stream)
        .collect(Collectors.toList());
  }

  @Override
  public List<MethodNameToQuerySort> getSorts() {
    if (StringUtils.isEmpty(this.orderByString)) {
      return new ArrayList<>();
    }
    return Arrays.stream(KEYWORD_ASC_DESC_SPLIT_PATTERN.split(this.orderByString))
        .map(
            propertyAndDirectionString -> {
              Matcher matcher = KEYWORD_DIRECTION_SPLIT_PATTERN.matcher(propertyAndDirectionString);
              if (matcher.find() == false) {
                throw new IllegalArgumentException(
                    String.format(INVALID_ORDER_SYNTAX, propertyAndDirectionString));
              }
              String columnString = matcher.group(1);
              String directionString = matcher.group(2);

              // No property, but only a direction keyword
              if (DIRECTION_KEYWORDS.contains(columnString) && directionString == null) {
                throw new IllegalArgumentException(
                    String.format(INVALID_ORDER_SYNTAX, propertyAndDirectionString));
              }
              return MethodNameToQuerySort.builder()
                  .propertyPathMeta(columnString)
                  .asc(directionString == null || directionString.equals("Asc"))
                  .build();
            })
        .collect(Collectors.toList());
  }

  private RelationalOpEnum resolveRelationalOp(String filterWithKeyword) {
    return Arrays.stream(RelationalOpEnum.values())
        .filter(
            p -> p.getKeywords().stream().anyMatch(keyword -> filterWithKeyword.endsWith(keyword)))
        .findFirst()
        .orElse(RelationalOpEnum.EQUALS);
  }

  private String resolveColumn(String filterWithKeyword) {
    for (RelationalOpEnum relationalOp : RelationalOpEnum.values()) {
      for (String keyword : relationalOp.getKeywords()) {
        if (filterWithKeyword.endsWith(keyword)) {
          return filterWithKeyword.substring(0, filterWithKeyword.length() - keyword.length());
        }
      }
    }
    return filterWithKeyword;
  }
}
